﻿#region File Info Header
/*________________________________________________________________________________________

  Copyright (C) 2011 Jason Zhang, eagleboost@msn.com

  * THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, 
  * EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED 
  * WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.

________________________________________________________________________________________*/
#endregion File Info Header

namespace EagleBoost.Wpf.Presentation.Controls.NumericUpDown
{
  using System;
  using System.Reflection;
  using System.Windows;
  using System.Windows.Controls;
  using System.Windows.Controls.Primitives;
  using System.Windows.Data;
  using System.Windows.Input;
  using Behaviors;
  using Converters;
  using Extensions;

  /// <summary>
  /// NumericUpDown control
  /// 1. Minimum/Maximum/SmallChange/LargeChange/Value
  /// 2. Pasting validation support
  /// 3. Decimal places
  /// </summary>
  [TemplatePart(Name = PartTextBox, Type = typeof(TextBox))]
  [TemplatePart(Name = PartScrollBar, Type = typeof(ScrollBar))]
  public class NumericUpDown : Control
  {
    #region Constants
    private const string PartTextBox = "PART_TextBox";
    private const string PartScrollBar = "PART_ScrollBar";
    private const string DoubleValuePattern = @"^\d+(,\d{3,4})*(?:\.\d{0,X})?$";
    private const string IntegerValuePattern = @"^\d+(,\d{3,4})*$";
    private const string ControlXamlFile = "component/Controls/NumericUpDown/NumericUpDown.xaml";
    #endregion Constants

    #region Declarations
    private static readonly Style ControlStyle;
    #endregion Declarations

    #region ctors
    static NumericUpDown()
    {
      var assembly = System.IO.Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().ManifestModule.Name);
      var rd = Application.LoadComponent(new Uri(string.Format("{0};{1}", assembly, ControlXamlFile), UriKind.Relative)) as ResourceDictionary;
      if (rd != null)
      {
        ControlStyle = rd["NumericUpDown"] as Style;
      }

      StyleProperty.OverrideMetadata(typeof(NumericUpDown), new FrameworkPropertyMetadata(ControlStyle));
      DefaultStyleKeyProperty.OverrideMetadata(typeof(NumericUpDown), new FrameworkPropertyMetadata(typeof(NumericUpDown)));
      
      InitializeCommandBindings<NumericUpDown>();
    }

    public NumericUpDown()
    {
      this.InvokeOnLoaded(() => TextBox.Padding = new Thickness(1, 1, Spinner.ActualWidth, 1));
      GotFocus += (s, e) => TextBox.Focus();
    }

    private static void InitializeCommandBindings<T>() where T : NumericUpDown
    {
      SmallIncreaseBinding = new InputBinding(new RoutedCommand("SmallIncrease", typeof(T)), new KeyGesture(Key.Up));
      SmallIncreaseBinding.Register<T>((s, e) => ((T)s).Increase(((T)s).SmallChange));

      SmallDecreaseBinding = new InputBinding(new RoutedCommand("SmallDecrease", typeof(T)), new KeyGesture(Key.Down));
      SmallDecreaseBinding.Register<T>((s, e) => ((T)s).Decrease(((T)s).SmallChange));

      LargeIncreaseBinding = new InputBinding(new RoutedCommand("LargeIncrease", typeof(T)), new KeyGesture(Key.Up, ModifierKeys.Control));
      LargeIncreaseBinding.Register<T>((s, e) => ((T)s).Increase(((T)s).LargeChange));

      LargeDecreaseBinding = new InputBinding(new RoutedCommand("LargeDecrease", typeof(T)), new KeyGesture(Key.Down, ModifierKeys.Control));
      LargeDecreaseBinding.Register<T>((s, e) => ((T)s).Decrease(((T)s).LargeChange));
    }
    #endregion ctors

    #region Statics
    public static double Epsilon
    {
      get { return 1E-10; }
    }

    public static InputBinding SmallIncreaseBinding { get; private set; }

    public static InputBinding SmallDecreaseBinding { get; private set; }

    public static InputBinding LargeIncreaseBinding { get; private set; }

    public static InputBinding LargeDecreaseBinding { get; private set; }
    #endregion Statics

    #region DependencyProperties

    public static readonly DependencyProperty DecimalPlaceProperty =
      DependencyProperty.RegisterAttached("DecimalPlace", typeof(int), typeof(NumericUpDown),
                                          new PropertyMetadata(0, DecimalPlaceChangedCallback, DecimalPlaceCoerceValueCallBack));

    /// <summary>
    /// Decimal place of the value, 0 by default
    /// </summary>
    public int DecimalPlace
    {
      get { return (int)GetValue(DecimalPlaceProperty); }
      set { SetValue(DecimalPlaceProperty, value); }
    }

    public static readonly DependencyProperty LargeChangeProperty =
      DependencyProperty.RegisterAttached("LargeChange", typeof (double), typeof (NumericUpDown),
                                          new PropertyMetadata(5.0));

    /// <summary>
    /// Large change of the value when Increase/Decrease, 5 by default
    /// </summary>
    public double LargeChange
    {
      get { return (double)GetValue(LargeChangeProperty); }
      set { SetValue(LargeChangeProperty, value); }
    }

    public static readonly DependencyProperty MaximumProperty =
      DependencyProperty.RegisterAttached("Maximum", typeof (double), typeof (NumericUpDown),
                                          new PropertyMetadata(double.MaxValue));

    /// <summary>
    /// Maximum of the value when Increase/Decrease, max value of double by default
    /// </summary>
    public double Maximum
    {
      get { return (double)GetValue(MaximumProperty); }
      set { SetValue(MaximumProperty, value); }
    }

    public static readonly DependencyProperty MinimumProperty =
      DependencyProperty.RegisterAttached("Minimum", typeof (double), typeof (NumericUpDown),
                                          new PropertyMetadata(0.0));

    /// <summary>
    /// Minimum of the value when Increase/Decrease, 0 by default
    /// </summary>
    public double Minimum
    {
      get { return (double)GetValue(MinimumProperty); }
      set { SetValue(MinimumProperty, value); }
    }

    /// <summary>
    /// Small change of the value when Increase/Decrease, 1 by default
    /// </summary>
    public static readonly DependencyProperty SmallChangeProperty =
      DependencyProperty.RegisterAttached("SmallChange", typeof (double), typeof (NumericUpDown),
                                          new PropertyMetadata(1.0));

    public double SmallChange
    {
      get { return (double)GetValue(SmallChangeProperty); }
      set { SetValue(SmallChangeProperty, value); }
    }

    public static readonly DependencyProperty ValueProperty =
      DependencyProperty.RegisterAttached("Value", typeof (double), typeof (NumericUpDown),
                                          new UIPropertyMetadata(0.0, ValueChangedCallback));

    /// <summary>
    /// The Value, 0 by default
    /// </summary>
    public double Value
    {
      get { return (double)GetValue(ValueProperty); }
      set { SetValue(ValueProperty, value); }
    }

    public static readonly DependencyProperty ValueAlignmentProperty =
      DependencyProperty.RegisterAttached("ValueAlignment", typeof (TextAlignment), typeof (NumericUpDown),
                                          new UIPropertyMetadata(TextAlignment.Right));

    /// <summary>
    /// Alignment of the value, Right by default
    /// </summary>
    public TextAlignment ValueAlignment
    {
      get { return (TextAlignment)GetValue(ValueAlignmentProperty); }
      set { SetValue(ValueAlignmentProperty, value); }
    }
    #endregion DependencyProperties

    #region Events
    public static RoutedEvent ValueChangedEvent = EventManager.RegisterRoutedEvent(
      "ValueChanged", RoutingStrategy.Bubble, typeof (RoutedPropertyChangedEventHandler<double>), typeof (NumericUpDown));

    /// <summary>
    /// Occurs when value of this control has been changed
    /// </summary>
    public event RoutedPropertyChangedEventHandler<double> ValueChanged
    {
      add { AddHandler(ValueChangedEvent, value); }
      remove { RemoveHandler(ValueChangedEvent, value); }
    }
    #endregion Events

    #region Public Properties
    public TextBox TextBox
    {
      get { return GetTemplateChild(PartTextBox) as TextBox; }
    }

    public ScrollBar Spinner
    {
      get { return GetTemplateChild(PartScrollBar) as ScrollBar; }
    }
    #endregion Public Properties

    #region Private Methods
    private void UpdateDecimalPlace()
    {
      TextBox.SetValue(TextBoxMaskBehavior.MaskProperty,
        DecimalPlace > 0 ? DoubleValuePattern.Replace("X", DecimalPlace.ToString()) : IntegerValuePattern);
      var binding = new Binding("Value")
                      {
                        Converter = new NumericFormatConverter(),
                        ConverterParameter = "{0:N" + DecimalPlace + "}",
                        RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
                        Mode = BindingMode.TwoWay,
                        UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
                      };
      TextBox.SetBinding(TextBox.TextProperty, binding);
    }
    #endregion Private Methods

    #region Overrides
    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();

      if (TextBox != null && Spinner != null)
      {
        UpdateDecimalPlace();

        TextBox.InputBindings.Add(SmallIncreaseBinding);
        TextBox.InputBindings.Add(SmallDecreaseBinding);
        TextBox.InputBindings.Add(LargeIncreaseBinding);
        TextBox.InputBindings.Add(LargeDecreaseBinding);
      }
    }
    #endregion Overrides

    #region Event Handlers
    private static void DecimalPlaceChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
      var numericUpDown = (NumericUpDown)obj;
      numericUpDown.InvokeOnLoaded(numericUpDown.UpdateDecimalPlace);
    }

    private static object DecimalPlaceCoerceValueCallBack(DependencyObject obj, object baseValue)
    {
      return (baseValue != null && double.Parse(baseValue.ToString()) >= 0) ? baseValue : 0;
    }

    private static void ValueChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
      var numericUpDown = (NumericUpDown)obj;
      numericUpDown.RaiseValueChangedEvent((double)args.OldValue, (double)args.NewValue);
    }
    #endregion Event Handlers

    #region Public Methods
    public void RaiseValueChangedEvent(double oldValue, double newValue)
    {
      var valueArgs = new RoutedPropertyChangedEventArgs<double>(oldValue, newValue, ValueChangedEvent);
      RaiseEvent(valueArgs);
    }

    public void Increase(double change)
    {
      var num = Math.Min(Value + change, Maximum);
      if (Math.Abs(Value - num) > Epsilon)
      {
        Value = num;
      }
    }

    public void Decrease(double change)
    {
      var num = Math.Max(Value - change, Minimum);
      if (Math.Abs(Value - num) > Epsilon)
      {
        Value = num;
      }
    }
    #endregion Public Methods
  }
}
